<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

include_once 'library/spica/core/validator/Common.php';

/**
 * The date range validator ensures that the date value of a given string is not
 * before a certain date and/or after than a certain date.
 *
 * The date range validator checks a "minDate" date to a "maxDate" date to ensure that
 * the "maxDate" date is equal to or after the "minDate" date.
 * If not, the "InvalidArgumentException" is reported.
 *
 * This validator does not check the validity of the string to compare.
 *
 * @category   spica
 * @package    core
 * @subpackage validator\datetime
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.2
 * @since      March 22, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: DateTime.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaDateRangeValidator extends SpicaRangeValidator
{
    /**
     * The minimum date object.
     *
     * @var DateTime
     */
    protected $_minDate;

    /**
     * Minimum date in string.
     *
     * @var string
     */
    protected $_minDateInput;

    /**
     * The maximum date object.
     *
     * @var DateTime
     */
    protected $_maxDate;

    /**
     * Maximum date in string.
     *
     * @var string
     */
    protected $_maxDateInput;

    /**
     * The date time format to compare.
     *
     * @var string
     */
    protected $_formatToCompare = 'YmdHis';

    /**
     * Creates a validator with the provided violation message.
     *
     * @throws Exception if $minDate, $maxDate can not be parsed
     * @throws InvalidArgumentException if $minDate, $maxDate both are null or $rangeBoundaryType value is not valid.
     * @param  string $violationMessage violation message to use if the validation fails
     * @param  string $minDate minimum date. Required format: YYYY-MM-DD or YYYY-MM-DD HH::MI:SS.
     *                Defaults to null means that date to compare must be smaller than $maxDate,
     *                otherwise date to compare must be smaller than $maxDate and greater than $minDate
     * @param  string $maxDate maximum date. Required format: YYYY-MM-DD or YYYY-MM-DD HH::MI:SS.
     *                Defaults to null means that date to compare must be greater than $minDate,
     *                otherwise date to compare must be smaller than $maxDate and greater than $minDate
     * @param  int    $rangeBoundaryType This parameter determines how the value
     *                will be compared and can have 2 values.
     *                  SpicaRangeBoundaryType::INCLUSIVE - The value is included in the specified range
     *                  SpicaRangeBoundaryType::EXCLUSIVE - The value is not included in the specified range
     * @param  bool   $required Determines whether this value is required. Defaults to true
     */
    public function __construct($violationMessage, $minDate = null, $maxDate = null, $rangeBoundaryType = 1, $required = true)
    {
        if (SpicaRangeBoundaryType::INCLUSIVE !== $rangeBoundaryType && SpicaRangeBoundaryType::EXCLUSIVE !== $rangeBoundaryType)
        {
            throw new InvalidArgumentException('The $rangeBoundaryType argument must be 0 or 1.');
        }

        if (null === $minDate && null === $maxDate)
        {
            throw new InvalidArgumentException('Arguments $minDate and $maxDate cannot be null at the same time. ');
        }

        $this->_rangeBoundaryType = $rangeBoundaryType;

        if (null !== $minDate)
        {
            try
            {
                $this->_minDate      = new DateTime($minDate);
                $this->_minDateInput = $minDate;
            }
            catch (Exception $e)
            {
                throw new InvalidArgumentException('Argument $minDate is not valid. '.$e->getMessage());
            }
        }

        if (null !== $maxDate)
        {
            try
            {
                $this->_maxDate      = new DateTime($maxDate);
                $this->_maxDateInput = $maxDate;
            }
            catch (Exception $e)
            {
                throw new InvalidArgumentException('Argument $maxDate is not valid. '.$e->getMessage());
            }
        }

        if (null !== $this->_maxDate && null !== $this->_minDate &&
        $this->_maxDate->format($this->_formatToCompare) < $this->_minDate->format($this->_formatToCompare))
        {
            throw new InvalidArgumentException('Argument $maxDate must be greater than or equal argument $minDate. ');
        }

        $this->_violationMessage = $violationMessage;
        $this->_required         = (bool) $required;
    }

    /**
     * Sets date time format to compare.
     *
     * @param string $format
     */
    public function setFormat($format)
    {
        $this->_formatToCompare = $format;
    }

    /**
     * (non-PHPdoc)
     * @see library/spica/core/validator/SpicaValidator#isValid()
     */
    public function isValid($value = null)
    {
        $this->_value = $value;

        // No data is provided but this value is optional (not mandatory)
        if (true === spica_valid_empty_string($value) && false === $this->_required)
        {
            return true;
        }

        if (false === is_string($value))
        {
            return false;
        }

        try
        {
            $inputDate = new DateTime($value);
        }
        catch (Exception $e)
        {
            // Invalid date formatted string
            return false;
        }

        $format  = $this->_formatToCompare;
        $date    = $inputDate->format($format);

        // No maximum limitation
        if (null === $this->_maxDate)
        {
            if (SpicaRangeBoundaryType::INCLUSIVE === $this->_rangeBoundaryType)
            {
                return $this->_minDate->format($format) <= $date;
            }

            return $this->_minDate->format($format) < $date;
        }

        // No minimum limitation
        if (null   === $this->_minDate)
        {
            if (SpicaRangeBoundaryType::INCLUSIVE === $this->_rangeBoundaryType)
            {
                return $this->_maxDate->format($format) >= $date;
            }

            return $this->_maxDate->format($format) > $date;
        }

        if (SpicaRangeBoundaryType::INCLUSIVE === $this->_rangeBoundaryType)
        {
            return ($this->_minDate->format($format) <= $date && $this->_maxDate->format($format) >= $date);
        }

        return ($this->_minDate->format($format) > $date || $this->_maxDate->format($format) < $date);
    }

    /**
     * Gets the minimum value for the date.
     *
     * @return string representing the minimum date.
     */
    public function getMinValue()
    {
        return $this->_minDateInput;
    }

    /**
     * Gets the maximum value for the date.
     *
     * @return string representing the maximum date.
     */
    public function getMaxValue()
    {
        return $this->_maxDateInput;
    }
}

/**
 * The <code>SpicaDateStringValidator</code> class validates that a given value
 * contains a proper date and matches a specified format.
 *
 * @category   spica
 * @package    core
 * @subpackage validator\datetime
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.2
 * @since      March 22, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: DateTime.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaDateStringValidator extends SpicaValidator
{
    /**
     * Date string format to validate against.
     *
     * @var string
     */
    protected $_format;

    /**
     * Creates a validator with the provided violation message.
     *
     * @param string $violationMessage violation message to use if the validation fails
     * @param string $format the format of the date to validate against. If it is null, validator will
     *               validate against US date format: 'YYYY-MM-DD'
     * @param bool   $required Determines whether this value is required. Defaults to true
     */
    public function __construct($violationMessage, $format = null, $required = true)
    {
        if (null === $format)
        {
            $format = 'YYYY-MM-DD';
        }

        if (false === is_string($format) || empty($format))
        {
            throw new InvalidArgumentException('Argument $format value is not valid. A non-empty string is expected. ');
        }

        $this->_format           = $format;
        $this->_violationMessage = $violationMessage;
        $this->_required         = (bool) $required;
    }

    /**
     * (non-PHPdoc)
     * @see library/spica/core/validator/SpicaValidator#isValid()
     */
    public function isValid($value = null)
    {
        $this->_value = $value;

        // No data is provided but this value is optional (not mandatory)
        if (true === spica_valid_empty_string($value) && false === $this->_required)
        {
            return true;
        }

        return spica_valid_date_format($value, $this->_format, true);
    }
}

/**
 * The SpicaDateDifferenceValidator takes a date and performs an operation
 * (compares against a fixed date value) to determine if the dates are separated
 * by the choosen number of days.
 *
 * For example, test that a given date are less than 60 days apart as compared to
 * a fixed date.
 *
 * This validator does not ensure that user input is a valid date string
 *
 * @category   spica
 * @package    core
 * @subpackage validator\datetime
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 02, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: DateTime.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaDateDifferenceValidator extends SpicaValidator
{
    /**
     * Date to compare.
     *
     * @var string
     */
    protected $_dateToCompare;

    /**
     * The number of dates apart.
     *
     * @var string
     */
    protected $_diff;

    /**
     * Operator for comparison.
     *
     * @var int
     */
    protected $_operator;

    /**
     * Creates a validator with the provided violation message.
     *
     * @throws Exception if $milestoneDate can not be parsed
     * @throws InvalidArgumentException if $milestoneDate is not a non-empty string
     * @param  string $violationMessage violation message to use if the validation fails
     * @param  string $dateToCompare the constant date value to compare with the date being validated.
     *                Defaults to null, means current date.
     * @param  int    $diff The number of days apart like 8 or -10
     * @param  string $operator contains comparison operation to perform. @see SpicaOperator
     * @param  bool   $required Determines whether this value is required. Defaults to true
     */
    public function __construct($violationMessage, $dateToCompare = null, $diff, $operator = null, $required = true)
    {
        if (null === $operator)
        {
            $operator = SpicaOperator::EQUAL;
        }

        if (false === SpicaOperator::isDefined($operator))
        {
            throw new InvalidArgumentException('Argument $operator value is not valid. ');
        }

        $this->_operator         = $operator;
        $this->_diff             = $diff;
        $this->_dateToCompare    = $dateToCompare;
        $this->_violationMessage = $violationMessage;
        $this->_required         = (bool) $required;
    }

    /**
     * (non-PHPdoc)
     * @see library/spica/core/validator/SpicaValidator#isValid()
     */
    public function isValid($value = null)
    {
        $this->_value = $value;

        try
        {
            // This validator does not ensure that $value is a valid date string
            $inputDate = new DateTime($value);
            // If developer does specify a certain date to compare
            if (null   === $this->_dateToCompare)
            {
                $compareDate = new DateTime('now');
            }
            else
            {
                $compareDate = new DateTime($this->_dateToCompare);
            }

            $ts1   = $inputDate->format('Y-m-d');
            $ts2   = $compareDate->format('Y-m-d');
            $diff  = abs(strtotime($ts1) - strtotime($ts2));
            $diff /= 3600*24;

            switch ($this->_operator)
            {
                case SpicaOperator::EQUAL:
                    return ($this->_diff === $diff);

                case SpicaOperator::GREATER_EQUAL:
                    return ($this->_diff >= $diff);

                case SpicaOperator::GREATER_THAN:
                    return ($this->_diff > $diff);

                case SpicaOperator::LESS_EQUAL:
                    return ($this->_diff <= $diff);

                case SpicaOperator::LESS_THAN:
                    return ($this->_diff < $diff);

                case SpicaOperator::NOT_EQUAL:
                    return ($this->_diff !== $diff);
            }
        }
        catch (Exception $e)
        {
            throw new SpicaValidatorException();
        }
    }
}

/**
 * A validator that evaluates two user input fields to determine if the difference
 * between their two values is a certain number apart from each other. For example,
 * test that two dates are less than 60 days apart.
 *
 * @category   spica
 * @package    core
 * @subpackage validator\datetime
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 02, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: DateTime.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaDateCompareValidator extends SpicaValidator
{
    /**
     * Milestone date.
     *
     * @var string
     */
    protected $_milestoneDate;

    /**
     * Field to validate.
     *
     * @var string
     */
    protected $_fieldToValidate;

    /**
     * Field to compare.
     *
     * @var string
     */
    protected $_fieldToCompare;

    /**
     * The number of dates apart.
     *
     * @var string
     */
    protected $_diff;

    /**
     * Operator for comparison.
     *
     * @var int
     */
    protected $_operator;

    /**
     * Creates a validator with the provided violation message.
     *
     * @param  string $fieldToValidate contains the date field name that holds the value to validate.
     * @param  string $fieldToCompare contains the date field name that holds the value to
     *                compare with the date control being validated.
     * @param  string $diff contains a constant number of days to compare with the value
     * @param  string $operator contains comparison operation to perform. @see SpicaOperator
     *                entered by the user into the date field being validated.
     * @param  string $violationMessage violation message to use if the validation fails
     * @param  bool   $required Determines whether this value is required. Defaults to true
     */
    public function __construct($fieldToValidate, $fieldToCompare, $diff, $operator = null, $violationMessage, $required = true)
    {
        if (null === $operator)
        {
            $operator = SpicaOperator::EQUAL;
        }

        if (false === SpicaOperator::isDefined($operator))
        {
            throw new InvalidArgumentException('Argument $operator value is not valid. ');
        }

        $this->_operator         = $operator;
        $this->_diff             = $diff;
        $this->_fieldToValidate  = $fieldToValidate;
        $this->_fieldToCompare   = $fieldToCompare;
        $this->_violationMessage = $violationMessage;
        $this->_required         = (bool) $required;
    }

    /**
     * (non-PHPdoc)
     * @see library/spica/core/validator/SpicaValidator#isValid()
     */
    public function isValid($value = null)
    {
        $this->_value = $value;

        if (!isset($_POST[$this->_fieldToValidate]) || !isset($_POST[$this->_fieldToCompare]))
        {
            return (false === $this->_required);
        }

        if (spica_valid_empty($_POST[$this->_fieldToValidate]) || spica_valid_empty($_POST[$this->_fieldToCompare]))
        {
            return (false === $this->_required);
        }

        $first  = $_POST[$this->_fieldToValidate];
        $second = $_POST[$this->_fieldToCompare];

        try
        {
            // This validator does not ensure that $value is a valid date string
            $inputDate   = new DateTime($first);
            $compareDate = new DateTime($second);

            $ts1   = $inputDate->format('Y-m-d');
            $ts2   = $compareDate->format('Y-m-d');
            $diff  = abs(strtotime($ts1) - strtotime($ts2));
            $diff /= 3600*24;

            switch ($this->_operator)
            {
                case SpicaOperator::EQUAL:
                    return ($this->_diff === $diff);

                case SpicaOperator::GREATER_EQUAL:
                    return ($this->_diff >= $diff);

                case SpicaOperator::GREATER_THAN:
                    return ($this->_diff > $diff);

                case SpicaOperator::LESS_EQUAL:
                    return ($this->_diff <= $diff);

                case SpicaOperator::LESS_THAN:
                    return ($this->_diff < $diff);

                case SpicaOperator::NOT_EQUAL:
                    return ($this->_diff !== $diff);
            }
        }
        catch (Exception $e)
        {
            throw new SpicaValidatorException();
        }
    }
}

/**
 * Returns true if the string in questions is in a valid date format.
 *
 * @param  string $string The string in question
 * @param  string $format The required date format. Defaults to null means that
 *                the US format 'YYYY-MM-DD' is used for checking.
 * @param  bool   $required Defaults to true
 * @return bool
 */
function spica_valid_date_format($string, $format = null, $required = true)
{
    if (false === is_string($string))
    {
        return false;
    }

    $string = (string) $string;

    if (''  === trim($string))
    {
        return (false === $required);
    }

    if (null === $format)
    {
        $format = 'YYYY-MM-DD';
    }

    // Do not use strtotime() and date_parse(). It will lead to unpredicted results.
    // Do not preg_split() here for performance consideration
    switch ($format)
    {
        case 'YYYY/MM/DD':

            $parts = explode('/', $string, 3);

            if (count($parts) < 3)
            {
                return false;
            }

            list($year, $month, $day) = $parts;
            break;

        case 'YYYY/MM/DD HH:MI:SS':

            $parts = explode(' ', $string, 2);

            if (2 !== count($parts))
            {
                return false;
            }

            $dateParts = explode('/', $parts[0], 3);
            if (count($dateParts) < 3)
            {
                return false;
            }

            list($year, $month, $day) = $dateParts;

            if (false === spica_valid_time_format($parts[1], 'HH:MI:SS', true))
            {
                return false;
            }

            break;

        case 'YYYY-MM-DD':

            $parts = explode('-', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($year, $month, $day) = $parts;
            break;

        case 'YYYY/DD/MM':

            $parts = explode('/', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($year, $day, $month) = $parts;
            break;

        case 'YYYY-DD-MM':

            $parts = explode('-', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($year, $day, $month) = $parts;
            break;

        case 'DD-MM-YYYY':

            $parts = explode('-', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($day, $month, $year) = $parts;
            break;

        case 'DD/MM/YYYY':

            $parts = explode('/', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($day, $month, $year) = $parts;
            break;

        case 'MM-DD-YYYY':

            $parts = explode('-', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($month, $day, $year) = $parts;
            break;

        case 'MM/DD/YYYY':

            $parts = explode('/', $string, 3);
            if (count($parts) < 3)
            {
                return false;
            }

            list($month, $day, $year) = $parts;
            break;

        case 'YYYYMMDD':

            $year  = substr($string, 0, 4);
            $month = substr($string, 4, 2);
            $day   = substr($string, 6, 2);
            break;

        case 'YYYYDDMM':

            $year  = substr($string, 0, 4);
            $day   = substr($string, 4, 2);
            $month = substr($string, 6, 2);
            break;

        default:
            throw new InvalidArgumentException('Argument $format value "'.$format.'" is not valid. ');
    }

    return checkdate($month, $day, $year);
}

/**
 * Checks if the given values can be combined into a valid time string.
 *
 * @param  string|int $hour
 * @param  string|int $minute
 * @param  string|int $second
 * @return bool
 */
function spica_valid_time($hour, $minute, $second)
{
    if (!spica_valid_integer($hour) || !spica_valid_integer($minute) || !spica_valid_integer($second))
    {
        return false;
    }

    $hour   = (int) $hour;
    $minute = (int) $minute;
    $second = (int) $second;

    if (($hour < 0 || $hour > 24) || ($minute < 0 || $minute > 60) || ($second < 0 || $second > 60))
    {
        return false;
    }

    return true;
}

/**
 * Returns true if it is a valid time format (hour, minute and second), false otherwise.
 *
 * @param  string $string
 * @param  string $format Defaults to null means that the US format 'HH:MI:SS' will be used
 * @param  bool   $required
 * @return bool
 */
function spica_valid_time_format($string, $format = null, $required = true)
{
    if (false === is_string($string))
    {
        return false;
    }

    if (null === $format)
    {
        $format = 'HH:MI:SS';
    }

    $string = (string) $string;

    if (''  === trim($string))
    {
        return (false === $required);
    }

    switch ($format)
    {
        case 'HH:MI:SS':

            $timeParts = explode(':', $string, 3);

            if (count($timeParts) < 3)
            {
                return false;
            }

            list($hour, $minute, $second) = $timeParts;
            break;

        case 'HH:MI':

            $timeParts = explode(':', $string, 2);

            if (count($timeParts) < 2)
            {
                return false;
            }

            list($hour, $minute) = $timeParts;
            $second = '00';
            break;

        default:
            throw new InvalidArgumentException('Time format '.$format.' is not valid. ');
    }

    return spica_valid_time($hour, $minute, $second);
}

/**
 * Returns true if it is a valid date (ISO 8601 format), false otherwise.
 *
 * @param  mixed $value
 * @return bool
 */
function spica_valid_date_iso8601($value)
{
    list($year, $month, $day) = sscanf($value, '%d-%d-%d');
    return (bool) checkdate($month, $day, $year);
}

/**
 * Checks to see if the date given is a valid format date.
 *
 * This function can validate the following format:
 *
 * + YYYY:  A full numeric representation of a year, 4 digits
 * + YY:    A two digit representation of a year
 * + MMMM:  A full textual representation of a month, such as January or March
 * + MMM:   A short textual representation of a month, three letters
 * + MM:    Numeric representation of a month, with leading zeros
 * + M1:    Numeric representation of a month, without leading zeros
 * + DDDD:  A full textual representation of the day of the week
 * + DDD:   A textual representation of a day, three letters
 * + DD:    Day of the month, 2 digits with leading zeros
 *
 * This function is ported partially from WVB web services functions written by pcdinh.
 *
 * FIXME: This function is not complete and does not work.
 *
 * @param  $string
 * @param  $format The date time format of the string
 * @return bool
 */
function spica_valid_date($string, $format)
{
    $map = array(
      'YYYY' => 'Y',
      'YY'   => 'y',
      'MMMM' => 'F',
      'MMM'  => 'M',
      'MM'   => 'm',
      'M1'   => 'n',
      'DDDD' => 'l', // lower-case L
      'DDD'  => 'D',
      'DD'   => 'd',
      'HH'   => 'H',
      'H1'   => 'G',
      'hh'   => 'h',
      'h1'   => 'g',
      'mm'   => 'i',
      'ss'   => 's',
      'AM'   => 'A',
      'am'   => 'a',
    );

    $format = str_replace(array_keys($map), array_values($map), $format);
    return (bool) date($format, strtotime($string));
}

?>